package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"math/rand"
	"net"
	"os"
	"path"
	"path/filepath"
	"time"

	"flag"

	"crypto/md5"

	"github.com/pkg/sftp"
	"golang.org/x/crypto/ssh"
)

//SFTP句柄
var sftpClient *sftp.Client

//日志文件句柄
var logfile *os.File

//日志文件对象
var logger *log.Logger

//传输循环次数初始化
var runtimes int

//传输线程开始结束标志位
var gogogo = false

//参数列表，说明及参数默认值
var (
	username  = flag.String("u", "nisedt", "Username")
	password  = flag.String("p", "Athene", "Password")
	ip        = flag.String("ip", "127.0.0.1", "IP Address")
	port      = flag.Int("port", 22, "Port")
	localDir  = flag.String("l", "/home/nisedt/Code/L", "local sync dir")
	remoteDir = flag.String("r", "/home/nisedt/Code/R", "remote sync dir")
)

func main() {
	//传输循环次数初始化
	runtimes = 0
	//获取参数
	flag.Parse()
	//创建日志文件并初始化日志对象
	t := time.Now().Format("2006-01-02 150405") + ".log"
	dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
	logfile, err = os.Create(path.Join(dir, t)) //os.OpenFile(path.Join(dir, t), os.O_RDWR|os.O_CREATE, 0666)
	if err != nil {
		fmt.Printf("%s\r\n", err.Error())
		os.Exit(-1)
	}
	logger = log.New(logfile, "\r\n", log.Ldate|log.Ltime|log.Llongfile)
	//以时间为种子生成[0-12]随机免授权文件运行次数
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	runt := r.Intn(12)
	//程序死循环
	for {
		//运行次数加一
		runtimes++
		//运行次数大于免授权文件使用次数时判断是否存在授权文件
		if runtimes > runt {
			//授权文件不存在时在日志中输出退出信息并停止运行
			if !zcm(dir) {
				logger.Println("No authorization file found! Software will be exit!")
				os.Exit(-1)
			}
		}
		//如果有线程在进行传输工作则跳过此次循环
		if !gogogo {
			go run()
		}
		//休眠5分钟
		time.Sleep(5 * time.Minute)
	}
}

//运行线程
func run() {
	//线程开始时置位线程运行标志位
	gogogo = true
	var err error
	//日志输出传输任务开始
	logger.Println("Task start !")
	//连接远程ssh服务器
	sftpClient, err = connect(*username, *password, *ip, *port)
	if err != nil {
		logger.Fatal(err)
	}
	//文件夹同步开始
	syncDir(*localDir, *remoteDir)
	//延迟关闭ssh连接
	defer sftpClient.Close()
	//日志输出任务结束
	logger.Println("Task finished !")
	//线程运行标志位复位
	gogogo = false
}

//传送文件
func sendFile(fileName string, localPath string, remotePath string) error {
	//获取本地文件句柄
	srcFile, err := os.Open(path.Join(localPath, fileName))

	if err != nil {
		//日志输出错误信息和跳过的文件路径及文件名，并结束该函数
		logger.Println(err)
		logger.Println("Pass " + path.Join(localPath, fileName) + " to remote server !")
		return err
	}
	//延迟关闭文件
	defer srcFile.Close()
	//获取远程文件名
	var remoteFileName = path.Base(fileName)
	//远程路径下创建同名文件
	dstFile, err := sftpClient.Create(path.Join(remotePath, remoteFileName))
	if err != nil {
		//日志输出错误信息并结束函数
		logger.Println(err)
		return err
	}
	//延迟关闭远程文件
	defer dstFile.Close()
	//获取本地文件大小
	FileLen, _ := fileSize(path.Join(localPath, fileName))
	var BufLen int64
	//每次传输的buff大小
	//当文件大于1024000时，每次传输数据为1024000
	//当文件小于等于1024000时，传输数据为文件大小
	if FileLen >= 1024000 {
		BufLen = 1024000
	} else {
		BufLen = FileLen
	}
	//创建缓冲区
	buf := make([]byte, BufLen)
	//循环发送数据直到文件结束
	for {
		n, _ := srcFile.Read(buf)
		if n == 0 {
			break
		}
		dstFile.Write(buf)
	}
	//日志输出传输完成的文件路径及文件名信息
	logger.Println("copy " + path.Join(localPath, fileName) + " to remote server finished!")
	return nil
}

//ssh连接参数设置
func connect(user, password, host string, port int) (*sftp.Client, error) {
	var (
		auth         []ssh.AuthMethod
		addr         string
		clientConfig *ssh.ClientConfig
		sshClient    *ssh.Client
		sftpClient   *sftp.Client
		err          error
	)
	// get auth method
	auth = make([]ssh.AuthMethod, 0)
	auth = append(auth, ssh.Password(password))

	clientConfig = &ssh.ClientConfig{
		User:    user,
		Auth:    auth,
		Timeout: 30 * time.Second,
		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
			return nil
		},
	}

	// connet to ssh
	addr = fmt.Sprintf("%s:%d", host, port)

	if sshClient, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
		return nil, err
	}

	// create sftp client
	if sftpClient, err = sftp.NewClient(sshClient); err != nil {
		return nil, err
	}

	return sftpClient, nil
}

//计算本地文件大小
func fileSize(file string) (int64, error) {
	f, e := os.Stat(file)
	if e != nil {
		return 0, e
	}
	return f.Size(), nil
}

//获取本地文件夹下的文件名及文件夹名
func walkLocalDir(dirPth string) (file []string, dir []string, err error) {
	all, err := ioutil.ReadDir(dirPth)
	if err != nil {
		return nil, nil, err
	}
	for _, fi := range all {
		//fmt.Printf("%+v\n", fi)
		if fi.IsDir() { // 忽略目录
			dir = append(dir, fi.Name())
		} else {
			file = append(file, fi.Name())
		}
	}
	return file, dir, err
}

//获取远程文件夹下文件名及文件夹名
func walkRemoteDir(dirPth string) (file []string, dir []string, err error) {
	tt, _ := sftpClient.ReadDir(dirPth)
	for _, v := range tt {
		//fmt.Printf("序号：%d内容：%+v\n", i, v)
		if v.IsDir() {
			dir = append(dir, v.Name())
		} else {
			file = append(file, v.Name())
		}
	}
	return file, dir, err
}

//同步本地目录下文件文件夹至远程文件夹下
func syncDir(localPath string, remotePath string) (lerr, rerr error) {
	//获取本地文件列表及文件夹列表
	localFileList, localDirList, lerr := walkLocalDir(localPath)
	//获取远程文件列表及文件夹列表
	remoteFileList, remoteDirList, rerr := walkRemoteDir(remotePath)
	if (lerr != nil) || (rerr != nil) {
		return lerr, rerr
	}
	//当本地文件列表不为空时，做本地文件列表和远程文件列表的对比
	if localFileList != nil {
		//外层遍历，遍历本地文件列表
		for _, v := range localFileList {
			//远程目录下是否存在本地文件名的标志位
			var flag bool
			//标志位初始化
			flag = false
			//内层遍历，遍历远程文件列表
			for _, i := range remoteFileList {
				//当发现本地文件名在远程目录下存在时，标志位置位并退出内层遍历
				if v == i {
					flag = true
					break
				}
			}
			//本地文件名在远程目录下不存在时发送本地文件到远程目录下
			if flag == false {
				sendFile(v, localPath, remotePath)
			}
		}
	}
	//当本地目录列表不为空时
	if localDirList != nil {
		//遍历外层，遍历本地目录列表
		for _, v := range localDirList {
			//本地文件夹名在远程是否存在标志位
			var flag bool
			//标志位初始化
			flag = false
			//内层遍历，遍历远程目录列表
			for _, i := range remoteDirList {
				//当发现本地文件夹名在远程目录下存在时，标志位置位并退出内层遍历
				if v == i {
					flag = true
					break
				}
			}
			//本地文件夹名在远程目录下不存在时在远程目录下创建同名文件夹名
			if flag == false {
				sftpClient.Mkdir(path.Join(remotePath, v))
				//日志输出创建的文件夹名
				logger.Println("MkDir " + v)
			}
			//目录嵌套同步
			syncDir(path.Join(localPath, v), path.Join(remotePath, v))
		}
	}
	return nil, nil
}

//注册码验证功能
func zcm(dir string) bool {
	var mac string
	var err error
	interfaces, err := net.Interfaces()
	if err != nil {
		panic("Poor soul, here is what you got:" + err.Error())
	}
	//将电脑的mac地址字符串连接到一起
	for _, inter := range interfaces {
		macx := inter.HardwareAddr
		mac = mac + macx.String()
	}
	//将mac字符串进行md5运算
	cc := md5.New()
	cc.Write([]byte(mac))
	dd := cc.Sum(nil)
	//打开授权文件
	f, err := os.OpenFile(path.Join(dir, "lic"), os.O_RDONLY, 0666)
	if err != nil {
		return false
	}
	filelen, _ := fileSize(path.Join(dir, "lic"))
	gg := make([]byte, filelen)
	f.Read(gg)
	//进行授权文件与计算的授权码进行校验
	for i, v := range gg {
		//println(dd[i], v)
		if v == dd[i] {
			continue
		} else {
			return false
		}
	}
	f.Close()
	//fmt.Printf("%s\n", dd)
	return true
}
